/*
  Copyright (C) 2011, Kyungjoo Kim
  
  This file is part of LINAL (LINear ALgebra)
  
  All rights reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  
  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
  
  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  Also add information on how to contact you by electronic and paper mail.
  
  
  Kyungjoo Kim
  iamkyungjoo@gmail.com
*/

#ifndef LINAL_DEVICE_GPU_OBJ_HXX
#define LINAL_DEVICE_GPU_OBJ_HXX

namespace linal    {
  namespace gpu  {

    typedef struct FLA_Obj_view FLA_Obj;    
    typedef class  Matrix_*     Matrix;
    typedef class  Flat_*       Flat;
    typedef class  Device_Obj_* Device_Obj;
    typedef class  Flat_Obj_*   Flat_Obj;

    // -------------------------------------------------------------- 
    // ** Flat Obj 
    /*!
      Flat_Obj_ class stores the buffer on the GPU memory
    */
    class Flat_Obj_ : public Device_Obj_ {
    private:
    protected:
      __device__ void *_buffer_device;

    public:

      /*!
        Default constructor.
      */
      Flat_Obj_() : Device_Obj_(),
                    _buffer_device(NULL) {  }

      /*!
        Wrapping constructor
        \param obj [in] Input FLA_Obj
      */
      Flat_Obj_(FLA_Obj obj) : Device_Obj_(obj),
                               _buffer_device(NULL) { }
      /*! 
        Copy constructor.
        \param obj [in] Input Flat_Obj_
      */
      Flat_Obj_(const Flat_Obj_& obj) : Device_Obj_(obj),
                                        _buffer_device(NULL)  {  }

      /*! Destructor.
       */
      virtual ~Flat_Obj_() { this->free(); }

      /*! Create buffer on the gpu
       */
      inline virtual void create_device_buffer() {
        LINAL_ERROR( !is_base_null(), 
                     LINAL_ERROR_BASE_OBJ_IS_NULL );

        cublasStatus status = CUBLAS_STATUS_SUCCESS;

        // create m by n matrix
        status = cublasAlloc( this->get_m()*this->get_n(),
                              this->get_datatype_size(),
                              (void**)&_buffer_device );

        LINAL_ERROR( status == CUBLAS_STATUS_SUCCESS,
                     LINAL_ERROR_CUBLAS_FAIL_TO_ALLOC );
        this->_buffer_device_created = true;
      }

      /*! Free buffer on the gpu
       */
      inline virtual void free_device_buffer() {

        cublasFree( _buffer_device );

        _buffer_device         = NULL;
        this->_buffer_device_created = false;
      }

      /*! Associate the buffer to the device.
        \param buf [in] pointer to the buffer to be associated.
       */
      inline virtual void  set_device_buffer(void *buf) { 
        _buffer_device = buf; 
      }

      /*! Check out the device buffer to use outside.
       */
      inline virtual void* get_device_buffer() { 
        return _buffer_device; 
      }

      /*!
        Move buffer from device to host.
        \param obj [in] FLA_Obj will read GPU buffer
      */
      inline virtual void get_matrix_to(FLA_Obj obj) {

        cublasStatus status = CUBLAS_STATUS_SUCCESS;

        // Read buffer on GPU to CPU
        status = cublasGetMatrix( this->get_m(),
                                  this->get_n(),
                                  this->get_datatype_size(),
                                  _buffer_device,
                                  this->get_m(),
                                  FLA_Obj_buffer_at_view( obj ),
                                  FLA_Obj_col_stride( obj ) );

        LINAL_ERROR( status == CUBLAS_STATUS_SUCCESS,
                     LINAL_ERROR_CUBLAS_FAIL_TO_GET_MATRIX );
      }

      /*!
        Move buffer from host to device.
        \param obj [in] FLA_Obj whose contenst will be written in GPU
      */
      inline virtual void set_matrix_from(FLA_Obj obj, Int_ &cache_hit) {

        cublasStatus status = CUBLAS_STATUS_SUCCESS;

        // check cache - buffer pointer, offset m and n, m and n
        if (this->is_dirty()) {
          cache_hit = false;

          // Write the CPU memory into GPU
          status = cublasSetMatrix( FLA_Obj_length( obj ),
                                    FLA_Obj_width( obj ),
                                    FLA_Obj_datatype_size( FLA_Obj_datatype( obj ) ),
                                    FLA_Obj_buffer_at_view( obj ), 
                                    FLA_Obj_col_stride( obj ),
                                    _buffer_device,
                                    this->get_m() );
          this->set_dirt(false);
        } else {
          cache_hit = true;
        }
        LINAL_ERROR( status == CUBLAS_STATUS_SUCCESS,
                     LINAL_ERROR_CUBLAS_FAIL_TO_SET_MATRIX );
      }
    };
  }
}

#endif
